Repository navigation
feat!: reuse freed memory after a sweep, and drop the analysis - #49
Merged
Merged
Conversation
A stray comment opener in weavec_sweep.c broke the Linux half of the sweep (-Wcomment under -Werror). clang-tidy: LibrarySpec.cpp parameter names and braces, unit-test findings, and modernize-use-designated-initializers off for the unit tests' positional expectation tables. Prettier on cli.md and guarantees.md.
…a phone's page skipInternal is used only by the Darwin region walk (-Wunused-function on Linux). The home page's demo is a grid item whose code block's longest line set the column's width, so the page overflowed at phone width and the header covered the search button (the mobile browser test).
glibc's inline fortify wrappers pass the rest of the object, not a field's size, so the tests of (F) spell the fortified call out as Darwin's headers produce it (RFC 0036 amendment 9, documented in the guarantees). With _FORTIFY_SOURCE=3 on glibc, longjmp is __longjmp_chk, and glibc's own memcpy wrapper replaces the case header's model of it (the case builds with -U_FORTIFY_SOURCE).
…an a shared stack mapping from its lowest On Linux every stopped thread records the handler's and the interrupted code's stack pointer, so more than 2,048 threads overran World::stacks. A mapping that holds several threads' stacks is now scanned from the lowest of their stack pointers, and the unread per-stack mapping bounds (and the pass over /proc/self/maps that filled them) are gone. RFC 0036 §3.4.
…r /dev, sweep for released huge blocks A Linux sweep that could not read /proc/self/maps scanned no mapping and still recycled; it now recycles nothing, as one that cannot stop every thread does, and Darwin's walk does the same when it ends early. The /dev exclusion also skipped POSIX shared memory (/dev/shm), shared anonymous mappings (/dev/zero) and hugetlbfs. Released huge blocks now count toward the quarantine's budget, so a program that frees only huge blocks sweeps and gives their address space back (128 releases of 600 MiB blocks swept 0 times, now 128). RFC 0036 §3.2-§3.4, amendment 10.
…pace by the real unshared size A precompiled header built by weavec-cc had ASan taken out of its options at the end of the unit, so every compile that read it with the same flags rejected it; only code generation takes it out now. The frame-sharing threshold (RFC 0036 amendment 8) counted each object's alignment as padding, so three 96-byte locals (448 bytes apart) estimated 544 and shared bytes, and a use after scope passed; the threshold now uses the size the unshared layout takes. RFC 0036 §2 (amendment 8), §5.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements RFC 0036, Deterministic heap reuse. Under v0.16.0, a use after free was caught only while the freed block sat in a 16 MiB quarantine; once the program allocated enough, the block went to a new owner and the stale pointer corrupted it silently. Now a released heap block is reused only after a sweep finds no pointer to it anywhere in the program. This PR also deletes the advisory ownership analysis, which on untuned code reported nothing worth fixing (62 of 69 findings false) and caught none of the bugs the guards miss.
The RFC's first draft also made spatial safety and use after return deterministic, through bounds from each pointer's provenance and locals moved into arena "frame blocks". That draft was built on this branch and measured at twice v0.16.0's run time (QuickJS 10.8×), so this revision keeps only the cheap parts. The RFC's Alternatives section records the per-mechanism measurement, and those features move to a later RFC that starts from a cost budget.
What a guarded program now gets:
strcpypast an 8-byte field into the next field traps asintra-object-overflow; v0.16.0 dropped the size.__SANITIZE_ADDRESS__and__has_feature(address_sanitizer), and the runtime implements ASan's poisoning interface, so code written to be ASan-clean is WeaveC-clean. In the corpus this removed jansson's deliberate hash over-read and SQLite's misuse-test trap, and pools that poison for ASan are now checked (use-after-poison).strto*/ato*parsers no longer require a terminator they never read.Measured (darwin-arm64; full table in the RFC's Measured gates)
Detection (G1–G3). Every determinism case this RFC catches stops, and every twin runs clean. The detection suites stop 105 of 109 bugs (v0.16.0: 104), and no twin stops. Verify mode reports no
weavec.provenon any case.Untuned programs (G4). CPython 3.13.7, SQLite 3.50.4 and coreutils 9.5 configure as with the reference compiler and pass as it does, with two exceptions:
test_faulthandler, whosetest_read_nullreads null on purpose and gets WeaveC's report;rawmemchrover-read, which ASan stops too.tr, SQLite'scapi3c,test_ast,test_functoolsandtest_regrtest, which failed under v0.16.0, now pass.Corpus (G5). Every config builds and passes its suite in trap and report mode with no trap. Injections stop 33 of 39, the same six misses as v0.16.0.
Run time (G6). The geometric mean is 1.05× v0.16.0's (1.61× the reference compiler). Every benchmark is within 1.25× its v0.16.0 ratio except QuickJS (see below).
Memory (G7). Peak RSS is within the limit on all 13 benchmarks: brotli 162 MiB (v0.16.0: 441), cmark 365 (473), libgit2 170 (117; limit 181).
Pauses (G8). The longest sweep with 256 MiB of live heap is 33 ms (limit 150).
Build (G9). Compile CPU is 0.86× v0.16.0's; no project is above 1.07×.
Not met, carried forward: QuickJS run time (G6). QuickJS runs at 1.31× its v0.16.0 ratio. Under
__SANITIZE_ADDRESS__it sends every object tomalloc(8.3 million allocations instead of 76,000), so each object is tracked separately. Built the same way (-D__SANITIZE_ADDRESS__for v0.16.0), v0.16.0 takes 3.2 s and this tree 2.72 s; with-fno-weavec-asan-compatit is 1.06× v0.16.0's.Linux (x86-64). Measured by this PR's CI only, because the local Docker daemon does not respond. The Release and Debug + ASan/UBSan jobs pass every test, including the sweep's Linux half (signal-stopped threads,
/proc/self/mapsroots) and the determinism cases in trap mode. Verify mode has not been run on Linux.The RFC stays Accepted, because two gates are not fully met: G6 (QuickJS, above) and G1's verify-mode run on Linux. RFC 0029 landed the same way.
Design notes
The sweep (§3.4,
runtime/weavec_sweep.c). It stops every other thread (Darwin:thread_suspend; Linux: a signal whose handler waits), scans registers, stacks, writable mappings and live heap blocks conservatively, and recycles quarantined slots that nothing marked.These two changes cut the fixed cost per sweep from 12–25 ms to about 1 ms (amendment 3).
Pacing (§3.3, amendment 1). A sweep is due at the soft budget
max(Q, live / 4), but waits until eight of the last sweep's durations have passed; the hard budgetmax(2 Q, live / 2)sweeps at once.Reuse. Recycling builds free lists in address order (amendment 4). Released slots of 64 KiB or more drop their pages at once, with a fresh mapping on Darwin, because
MADV_FREEstayed resident (amendment 5).WEAVEC_RT_STATS=1now also prints peak live and quarantined bytes.Frames (§2, amendment 8). RFC 0035's native frames are back, with
StackLifetime-based sharing. Sharing applies only above 512 bytes, so a use after scope in a small frame cannot land in a later local's bytes.Removed from the first draft: provenance bounds, escape checks and the escape registry, pointer slots, frame blocks and capture logs, the
setjmphook, the globals table and__weavec_rt_bounds.Released huge blocks (amendment 10). A released huge block counts toward the sweep's budget at its size until a sweep unmaps it. Before this, a program that freed only huge blocks never swept: 128 releases of 600 MiB blocks kept 75 GiB of address space reserved, and on Linux used one of the process's limited mappings each.
Amendments. All ten amendments in the RFC override its body. The rescope is described at the top of the RFC.
Fixed after review
/proc/self/mapsstill recycled. A read error part-way through, or Darwin's address-space walk ending early, now also counts as a failed scan./devis scanned on Linux:/dev/shm, shared anonymous mappings (listed as/dev/zero) and hugetlbfs. Other device mappings are still skipped.weavec-cchad ASan removed from its recorded options, so every compile that read it with the same flags rejected it. It now keeps them.Size. The library is 7,204 lines (budget 9,000) and the runtime 5,004 (budget 6,000). This PR adds 8.0K lines and deletes 85.5K.
Breaking changes:
The advisory analysis is deleted, with no aliases:
weavectool andweavec --whole-program;-fweavec-diagnose,-fweavec-budget=,-fweavec-unit-budget=,-fweavec-dump-analysisand-fweavec-analysis-stats=;The
-W…weavec…flags now reach Clang as unknown warning options.weavec.his version 1.0 and keeps onlyWEAVEC_UNSAFE. Code that uses another annotation no longer compiles.Units are compiled as ASan would compile them (
__SANITIZE_ADDRESS__is defined).-fno-weavec-asan-compatturns this off.Optimising glibc builds get
-D_FORTIFY_SOURCE=3unless the command line sets a level.Report kinds:
intra-object-overflowanduse-after-poisonare new;buffer-overflowis no longer reported.The runtime's entry points and its Darwin dispatch table (version 4) changed, so rebuild every object with this
weavec-cc.A program that frees heavily can hold more memory between sweeps (up to the hard budget). A block that a stale word still points to stays allocated.
On Linux the sweep uses
SIGPWR(orWEAVEC_RT_SWEEP_SIGNAL).Test plan
ctest -R '^runtime$': 29 tests, includingpacing,library-data-is-scanned,shared-memory-is-scanned,huge-releases-sweepand thesweep-*tests, in release and dev builds.lit build/dev/test: 49 tests, includingDriver/pch.candGuards/small-frame-scope.c.scripts/run-cases.py: 533 pass and 11XFAILin trap mode; verify mode reports noweavec.proven.ctest --preset dev: 39 of 39.scripts/check-hygiene.py,scripts/check-format.sh,scripts/check-cmake-format.sh, and clang-tidy on the changed C++ files.npm testandnpm run buildindocs/(71 pages validated).scripts/corpus-gate.py --full(G5),--quickratchet re-recorded, and--benchagainst v0.16.0 (G6, G7).